using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using GameEngine.Navigation;
using GameEngine.Units;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using SkinningInformation;
using GameEngine.Events;

namespace GameEngine
{
	public class World
	{
		private Dictionary<object, DoodadType> doodadTypes;
		private Dictionary<object, ParticleType> particleTypes;
		private Dictionary<object, ItemType> itemTypes;
		private Dictionary<object, UnitType> unitTypes;
		private Dictionary<object, WayPoint> points;

		private List<object> doodadTypeNames;
		private List<object> particleTypeNames;
		private List<object> itemTypeNames;
		private List<object> unitTypeNames;
		private List<object> pointNames;

		private IList<Unit> units;
		private IList<Particle> particles;
		private IList<Item> items;
		private IList<Doodad> doodads;

		private Queue<Unit> newUnits;
		private Queue<Particle> newParticles;
		private Queue<Item> newItems;
		private Queue<Doodad> newDoodads;

		private int updateSequence = 0;

		private Random random;

		public World()
		{
			this.random = new Random();

			this.doodadTypeNames = new List<object>();
			this.DoodadTypeNames = new ReadOnlyCollection<object>(this.doodadTypeNames);
			this.doodadTypes = new Dictionary<object, DoodadType>();

			this.particleTypeNames = new List<object>();
			this.ParticleTypeNames = new ReadOnlyCollection<object>(this.particleTypeNames);
			this.particleTypes = new Dictionary<object, ParticleType>();

			this.itemTypeNames = new List<object>();
			this.ItemTypeNames = new ReadOnlyCollection<object>(this.itemTypeNames);
			this.itemTypes = new Dictionary<object, ItemType>();

			this.unitTypeNames = new List<object>();
			this.UnitTypeNames = new ReadOnlyCollection<object>(this.unitTypeNames);
			this.unitTypes = new Dictionary<object, UnitType>();

			this.pointNames = new List<object>();
			this.PointNames = new ReadOnlyCollection<object>(this.pointNames);
			this.points = new Dictionary<object, WayPoint>();

			this.doodads = new List<Doodad>();
			this.newDoodads = new Queue<Doodad>();
			this.Doodads = new ReadOnlyCollection<Doodad>(this.doodads);

			this.particles = new List<Particle>();
			this.newParticles = new Queue<Particle>();
			this.Particles = new ReadOnlyCollection<Particle>(this.particles);

			this.items = new List<Item>();
			this.newItems = new Queue<Item>();
			this.Items = new ReadOnlyCollection<Item>(this.items);

			this.units = new List<Unit>();
			this.newUnits = new Queue<Unit>();
			this.Units = new ReadOnlyCollection<Unit>(this.units);
		}

		public NavigationMesh NavigationMesh { get; set; }
		public NavigationMap NavigationMap { get; set; }

		public ReadOnlyCollection<object> DoodadTypeNames { get; private set; }
		public ReadOnlyCollection<object> ParticleTypeNames { get; private set; }
		public ReadOnlyCollection<object> ItemTypeNames { get; private set; }
		public ReadOnlyCollection<object> UnitTypeNames { get; private set; }
		public ReadOnlyCollection<object> PointNames { get; private set; }

		public ReadOnlyCollection<Doodad> Doodads { get; private set; }
		public ReadOnlyCollection<Particle> Particles { get; private set; }
		public ReadOnlyCollection<Item> Items { get; private set; }
		public ReadOnlyCollection<Unit> Units { get; private set; }

		public TimeSpan WorldTime { get; private set; }

		public bool IsBusy { get; private set; } // When there were too many resurce requests and the update is taking too long the is busy goes true indicating units should not waste more time.

		public void RegisterDoodadType(object typeName, Model model)
		{
			DoodadType doodadType = new DoodadType() { Model = model };
			this.doodadTypes.Add(typeName, doodadType);
			this.doodadTypeNames.Add(typeName);
		}
		public void RegisterParticleType(object typeName, Model model, SkinningData skinningData, TimeSpan lifeTime)
		{
			// TODO: the particles may die on other conditions rather than lifetime. For example if it is a snow falling it would die when it reaches the grond...
			ParticleType particleType = new ParticleType() { Model = model, SkinningData = skinningData, LifeTime = lifeTime };
			this.particleTypes.Add(typeName, particleType);
			this.particleTypeNames.Add(typeName);
		}
		public void RegisterItemType(object typeName, Model model, SkinningData skinningData, float radius, bool autoPick, ICollection<Unit> pickableBy, Func<Unit, bool> unitFilter)
		{
			ItemType itemType = new ItemType() { Model = model, SkinningData = skinningData, Radius = radius, AutoPick = autoPick, PickableBy = pickableBy, UnitFilter = unitFilter };
			this.itemTypes.Add(typeName, itemType);
			this.itemTypeNames.Add(typeName);
		}
		public void RegisterUnitType(object typeName, Model model, SkinningData skinningData, float maxHitPoints, float movementSpeed, float radius)
		{
			UnitType unitType = new UnitType() { Model = model, SkinningData = skinningData, MaxHitPoints = maxHitPoints, MovementSpeed = movementSpeed, Radius = radius };
			this.unitTypes.Add(typeName, unitType);
			this.unitTypeNames.Add(typeName);
		}

		public void RegisterPoint(object pointName, Vector3 position)
		{
			WayPoint closestWaypoint = null;
			// Find the closest waypoint
			float distance = float.PositiveInfinity;
			foreach (WayPoint wayPoint in this.NavigationMap.WayPoints)
			{
				float newDistance = Vector3.Distance(wayPoint.Position, position);
				if (newDistance < distance)
				{
					closestWaypoint = wayPoint;
					distance = newDistance;
				}
			}
			// register
			this.points.Add(pointName, closestWaypoint);
			this.pointNames.Add(pointName);
		}

		public Doodad CreateDoodad(object typeName, Vector3 position, float rotation)
		{
			DoodadType doodadType = this.doodadTypes[typeName];
			Doodad doodad = new Doodad(doodadType, this) { Position = position, Rotation = rotation };
			this.newDoodads.Enqueue(doodad);
			return doodad;
		}
		public Particle CreateParticle(object typeName, Vector3 position, float rotation)
		{
			ParticleType particleType = this.particleTypes[typeName];
			Particle particle = new Particle(particleType, this, this.WorldTime) { Position = position, Rotation = rotation };
			this.newParticles.Enqueue(particle);
			return particle;
		}
		public Item CreateItem(object typeName, object pointName, float rotation, ICollection<Unit> pickableBy)
		{
			WayPoint wayPoint = this.points[pointName];
			ItemType itemType = this.itemTypes[typeName];
			Item item = new Item(itemType, this) { WayPoint = wayPoint, Position = wayPoint.Position, Rotation = rotation, PickableBy = pickableBy };
			this.newItems.Enqueue(item);
			return item;
		}
		public Unit CreateUnit(object typeName, object pointName, float rotation)
		{
			WayPoint wayPoint = this.points[pointName];
			UnitType unitType = this.unitTypes[typeName];
			Unit unit = new Unit(unitType, this) { HitPoints = unitType.MaxHitPoints, WayPoint = wayPoint, Position = wayPoint.Position, Rotation = rotation };
			this.newUnits.Enqueue(unit);
			return unit;
		}

		public void Update(GameTime gameTime)
		{
			AddQueueToList(this.doodads, this.newDoodads);
			AddQueueToList(this.particles, this.newParticles);
			AddQueueToList(this.items, this.newItems);
			AddQueueToList(this.units, this.newUnits);

			this.WorldTime = gameTime.TotalGameTime;
			// TODO: This resource handling SUX. BIG TIME! we'd better implement some mechanism to give tokens to units and when they consume them to put the units back on a queue waiting for the next access token...
			// Also such mechanism will be able to prioratize units and give more resources to one that are actively fighting over the one that are just crossing the map...

			// every time the update will start from the unit that first got busy so the next update probably some fresh time would be available for that unit
			int nextUpdateSequenceStart = 0;
			this.IsBusy = false;
			long updateStart = DateTime.Now.Ticks;
			
			for (int i = this.updateSequence; i < this.units.Count; i++)
			{
				Unit unit = this.units[i];
				unit.Update(gameTime);
				if (!this.IsBusy)
				{
					if (DateTime.Now.Ticks - updateStart > 100000)
					{
						this.IsBusy = true;
						nextUpdateSequenceStart = i + 1;
					}
				}
			}
			for (int i = 0; i < this.updateSequence && i < this.Units.Count; i++)
			{
				// Code copied... the best way to introduce bugs. extract into method... the only problem is that it would have way too silly name...
				Unit unit = this.units[i];
				unit.Update(gameTime);
				if (!this.IsBusy)
				{
					if (DateTime.Now.Ticks - updateStart > 100000)
					{
						this.IsBusy = true;
						nextUpdateSequenceStart = i + 1;
					}
				}
			}

			// TODO: Still there are micro freezes that look like a GarbageCollection .. or may be they are from the emulator?!? or debuger?!?

			// Remove zombies. TODO: we do not have to count the dead units always but since there are many ways for an unit to die (set hitpoints in code, hit, kill, DOTs...)
			List<Unit> decayedUnits = new List<Unit>();
			foreach (Unit unit in this.units)
			{
				if (unit.IsDecayed)
				{
					decayedUnits.Add(unit);
				}
			}
			foreach (Unit deadUnit in decayedUnits)
			{
				this.units.Remove(deadUnit);
			}

			this.updateSequence = nextUpdateSequenceStart;

			// Particles
			foreach (Particle particle in this.particles)
			{
				particle.Update(gameTime);
			}
			List<Particle> expiredParticles = new List<Particle>();
			foreach (Particle particle in this.particles)
			{
				if (particle.HasExpired)
				{
					expiredParticles.Add(particle);
				}
			}
			foreach (Particle deadParticle in expiredParticles)
			{
				this.particles.Remove(deadParticle);
			}

			// Items
			foreach (Item item in this.items)
			{
				item.Update(gameTime);
			}
			List<Item> pickedItems = new List<Item>();
			foreach (Item item in this.items)
			{
				if (item.IsPicked)
				{
					pickedItems.Add(item);
				}
			}
			foreach (Item item in pickedItems)
			{
				this.items.Remove(item);
			}
		}
		public void Draw(GameTime gameTime, GraphicsDevice graphicsDevice, ref Matrix view, ref Matrix projection)
		{
			BoundingFrustum boundingFrustum = new BoundingFrustum(view * projection);
			BlendState blendState = graphicsDevice.BlendState;
			// TODO: we need sprite patch here... we should be able to draw Sprites as well as Particles... the Particle is a 3d model while a Sprite is a 2D image or text drawn over the 3D models in screen coordinates
			graphicsDevice.BlendState = BlendState.Opaque;
			foreach (Doodad doodad in this.Doodads)
			{
				doodad.Draw(gameTime, ref view, ref projection, boundingFrustum);
			}
			foreach (Unit doodad in this.Units)
			{
				doodad.Draw(gameTime, ref view, ref projection, boundingFrustum);
			}
			graphicsDevice.BlendState = BlendState.Additive; // TODO: Some particles may use AdditiveBlend-ing...
			foreach (Particle particle in this.particles)
			{
				particle.Draw(gameTime, ref view, ref projection, boundingFrustum);
			}
			graphicsDevice.BlendState = BlendState.Additive;
			foreach (Item item in this.Items)
			{
				item.Draw(gameTime, ref view, ref projection, boundingFrustum);
			}
			graphicsDevice.BlendState = blendState;
		}

		// TODO: If no PahtFinding Resources are available for the rest of the current update make sure to create a queue and put the unit in it...
		internal IList<WayPoint> FindPath(Unit unit, WayPoint goal, float distance)
		{
			Vector3 unitPosition = unit.Position;
			IList<IPathBlocker> blockers = new List<IPathBlocker>();
			// TODO: Consider printing these on the map so they wont be checked upon each node and no lists would be generated here...
			foreach (Unit blockingUnit in this.Units)
			{
				if (!blockingUnit.IsDead)
				{
					float distanceToPathBlock = Vector3.Distance(blockingUnit.Position, unit.Position);
					if (distanceToPathBlock < unit.Radius + blockingUnit.Radius + (blockingUnit.IsMoving ? 40 : 80))
					{
						if (blockingUnit != unit)
						{
							blockers.Add(blockingUnit);
						}
					}
				}
			}
			return NavigationMap.FindPath(unit.WayPoint, goal, distance, unit.Radius, blockers);
		}
		internal bool CanMoveTo(Unit unit, Vector3 position)
		{
			foreach (IPathBlocker block in this.Units) // This should use the unit's waypoint positions instead...
			{
				if (unit != block && unit.Radius + block.Radius > Vector3.Distance(position, block.Position))
				{
					return false;
				}
			}
			return true;
		}

		// TODO: Add damage type descriptions. Slash, blunt, magic etc. defense, misses, criticle strickes etc... damage friends, foes, self?
		// Spread goes both sideways. if you spread a Pi/2 you hit all the area in front of you.
		public void SlashDamage(Unit unit, float damage, float range, float spread)
		{
			foreach (Unit target in this.Units)
			{
				if (target != unit)
				{
					float rangeToTarget = range + unit.Radius + target.Radius;
					if (Vector3.Distance(unit.Position, target.Position) <= rangeToTarget)
					{
						Vector3 targetDirection = target.Position - unit.Position;
						float unitRotation = MathHelper.WrapAngle(unit.Rotation);
						float targetAngle = (float)Math.Atan2(targetDirection.Y, targetDirection.X);
						// TODO: same code exist in MoveTo... for example this is about to be usefull and we should create our own MathHelper...
						if (MathHelper.Distance(unitRotation, targetAngle) > MathHelper.Pi)
						{
							if (targetAngle < unitRotation)
							{
								targetAngle += MathHelper.TwoPi;
							}
							else
							{
								targetAngle -= MathHelper.TwoPi;
							}
						}
						if (MathHelper.Distance(unitRotation, targetAngle) < spread)
						{
							this.Damage(unit, target, damage);
						}
					}
				}
			}
		}
		public void Damage(Unit unit, Unit target, float damage)
		{
			UnitInteractionArgs eventArgs = new UnitInteractionArgs(unit, target, damage);
			if (this.Damaging != null)
			{
				this.Damaging(this, eventArgs);
			}
			damage = eventArgs.Damage;

			bool alreadyDead = target.IsDead;
			target.HitPoints -= damage;
			if (target.HitPoints <= 0 && !alreadyDead)
			{
				this.Kill(unit, target, damage);
			}

			// TODO: The "Bleed" here should come from a parameter...
			this.CreateParticle("Bleed", target.Position, (float)random.NextDouble() * MathHelper.TwoPi);
			this.CreateParticle("Bleed", target.Position, (float)random.NextDouble() * MathHelper.TwoPi);
			this.CreateParticle("Bleed", target.Position, (float)random.NextDouble() * MathHelper.TwoPi);
		}
		public void Heal(Unit unit, Unit target, float healing)
		{
			UnitInteractionArgs eventArgs = new UnitInteractionArgs(unit, target, healing);
			if (this.Healing != null)
			{
				this.Healing(this, eventArgs);
			}
			healing = eventArgs.Damage;
			target.HitPoints = Math.Min(target.HitPoints + healing, target.MaxHitPoints); 

			// TODO: Add healing effect
		}
		public void Kill(Unit unit, Unit target, float damage)
		{
			UnitInteractionArgs eventArgs = new UnitInteractionArgs(unit, target, damage);
			target.IsDead = true;
			target.Action = new Die(target);
			if (this.Killing != null)
			{
				this.Killing(this, eventArgs);
			}

		}
		public void Pick(Unit unit, Item item)
		{
			item.IsPicked = true;
			if (this.Picking != null)
			{
				this.Picking(this, new ItemInteractionArgs(unit, item));
			}
		}

		public event ItemInteractionHandler Picking;
		public event UnitInteractionHandler Damaging;
		public event UnitInteractionHandler Killing;
		public event UnitInteractionHandler Healing;

		private static void AddQueueToList<T>(IList<T> list, Queue<T> queue)
		{
			while (queue.Count > 0)
			{
				list.Add(queue.Dequeue());
			}
		}
	}
}
